/*
 * graph2D
 * Copyright (c) 2009 Shun Moriya <shun@mnu.sakura.ne.jp>
 *
 * The MIT License
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "graphicDevice.h"
#include <math.h>
#include <stdio.h>

namespace Graph2D
{
	GLubyte GraphicDevice::colors[4 * GraphicDevice::VERTEX_MAX_COUNT];
	GLfloat GraphicDevice::coordinates[2 * GraphicDevice::VERTEX_MAX_COUNT];
	GLfloat GraphicDevice::verteces[2 * GraphicDevice::VERTEX_MAX_COUNT];
	GLsizei GraphicDevice::colorCount = 0;
	GLsizei GraphicDevice::coordinateCount = 0;
	GLsizei GraphicDevice::vertexCount = 0;

	Rect GraphicDevice::displayRect;
	float GraphicDevice::displayToScreenScale;

	Vector2 GraphicDevice::screenSize;
	float GraphicDevice::screenToDisplayScale;

	bool GraphicDevice::lastEnableAlpha = false;
	bool GraphicDevice::lastEnableDepth = false;
	bool GraphicDevice::lastEnableOpaque = false;
	bool GraphicDevice::lastEnableScissor = false;
	bool GraphicDevice::lastEnableTexture = false;
	bool GraphicDevice::lastEnableColorArray = false;
	bool GraphicDevice::lastEnableTextureCoordArray = false;
	Vector2 GraphicDevice::lastEnableScissorPosition;
	Vector2 GraphicDevice::lastEnableScissorSize;
	Color GraphicDevice::lastPrimitiveColor;
	GLenum GraphicDevice::lastPrimitiveMode;
	float GraphicDevice::lastAlphaFuncValue;
	unsigned char GraphicDevice::lastPrimitiveType = 0xFF;
	unsigned char GraphicDevice::lastAlphaFunc;
	unsigned char GraphicDevice::lastBlendMode;

	void GraphicDevice::initialize(const Vector2& deviceSize, const Vector2& screenSize)
	{
		GraphicDevice::screenSize = screenSize;
		const float scaleX = deviceSize.x / screenSize.x;
		const float scaleY = deviceSize.y / screenSize.y;
		screenToDisplayScale = (scaleX < scaleY) ? scaleX : scaleY;
		displayToScreenScale = 1.0f / screenToDisplayScale;
		const float positionX = (deviceSize.x / 2) - (screenSize.x * screenToDisplayScale / 2);
		const float positionY = (deviceSize.y / 2) - (screenSize.y * screenToDisplayScale / 2);
		displayRect.position.set(positionX, positionY);
		displayRect.size = deviceSize;

		colorCount = 0;
		coordinateCount = 0;
		vertexCount = 0;

		lastEnableAlpha = true;
		lastEnableDepth = false;
		lastEnableOpaque = true;
		lastEnableScissor = false;
		lastEnableTexture = false;
		lastEnableColorArray = false;
		lastEnableTextureCoordArray = false;

		lastPrimitiveMode = static_cast<GLenum>(-1);
		lastPrimitiveType = 0xFF;
		lastPrimitiveColor.set(1, 1, 1, 1);
		lastEnableScissorPosition.zero();
		lastEnableScissorSize.zero();

		GRAPH2D_CALL_GL(glEnable(GL_ALPHA_TEST));
		GRAPH2D_CALL_GL(glDisable(GL_DEPTH_TEST));
		GRAPH2D_CALL_GL(glDisable(GL_BLEND));
		GRAPH2D_CALL_GL(glDisable(GL_SCISSOR_TEST));
		GRAPH2D_CALL_GL(glDisable(GL_TEXTURE_2D));
		GRAPH2D_CALL_GL(glDisableClientState(GL_COLOR_ARRAY));
		GRAPH2D_CALL_GL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
		GRAPH2D_CALL_GL(glDisableClientState(GL_NORMAL_ARRAY));
		GRAPH2D_CALL_GL(glEnableClientState(GL_VERTEX_ARRAY));

		GRAPH2D_CALL_GL(glColor4f(lastPrimitiveColor.r, lastPrimitiveColor.g, lastPrimitiveColor.b, lastPrimitiveColor.a));

		lastAlphaFunc = ALPHA_FUNC_GREATER;
		lastAlphaFuncValue = 0.0f;
		GRAPH2D_CALL_GL(glAlphaFunc(GL_GREATER, lastAlphaFuncValue));

		lastBlendMode = BLEND_MODE_NORMAL;
		GRAPH2D_CALL_GL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));

		setScissor(lastEnableScissorPosition, screenSize);

		applyDrawArrayPointer();

		ortho2D();

		setClearColor(Color(0, 0, 0, 1));
#if defined(TARGET_IPHONE)
		GRAPH2D_CALL_GL(glClearDepthf(1.0f));
#elif defined(TARGET_WINDOWS)
		GRAPH2D_CALL_GL(glClearDepth(1.0));
#endif
		GRAPH2D_CALL_GL(glDepthFunc(GL_LESS));

		//glShadeModel(GL_SMOOTH);
	}

	void GraphicDevice::finalize()
	{
	}

	const Vector2& GraphicDevice::getScreenSize()
	{
		return screenSize;
	}

	float GraphicDevice::getScreenWidth()
	{
		return screenSize.x;
	}

	float GraphicDevice::getScreenHeight()
	{
		return screenSize.y;
	}

	float GraphicDevice::getScreenToDisplayScale()
	{
		return screenToDisplayScale;
	}

	const Vector2& GraphicDevice::getDisplaySize()
	{
		return displayRect.size;
	}

	float GraphicDevice::getDisplayWidth()
	{
		return displayRect.size.x;
	}

	float GraphicDevice::getDisplayHeight()
	{
		return displayRect.size.y;
	}

	float GraphicDevice::getDisplayToScreenScale()
	{
		return displayToScreenScale;
	}

	Vector2 GraphicDevice::translateScreenPosition(const Vector2& displayPosition)
	{
		Vector2 screenPosition;
		screenPosition.x = (displayPosition.x - displayRect.position.x) * displayToScreenScale;
		screenPosition.y = (displayPosition.y - displayRect.position.y) * displayToScreenScale;
		return screenPosition;
	}

	Vector2 GraphicDevice::translateDisplayPosition(const Vector2& screenPosition)
	{
		Vector2 displayPosition;
		displayPosition.x = displayRect.position.x + screenPosition.x * screenToDisplayScale;
		displayPosition.y = displayRect.position.y + screenPosition.y * screenToDisplayScale;
		return displayPosition;
	}

	void GraphicDevice::applyDrawArrayPointer()
	{
		flush();

		GRAPH2D_CALL_GL(glColorPointer(4, GL_UNSIGNED_BYTE, 0, colors));
		GRAPH2D_CALL_GL(glTexCoordPointer(2, GL_FLOAT, 0, coordinates));
		GRAPH2D_CALL_GL(glVertexPointer(2, GL_FLOAT, 0, verteces));
	}

	void GraphicDevice::viewport(const int x, const int y, const int width, const int height)
	{
		GRAPH2D_CALL_GL(glViewport(x, y, width, height));
	}

	void GraphicDevice::frustum(const float fov, const float n, const float f)
	{
		glMatrixMode(GL_PROJECTION);
		const GLfloat aspect = displayRect.size.x / displayRect.size.y;
		const GLfloat width = n * tanf(3.14159265358979323846f * fov / 180.0f / 2.0f);
		glLoadIdentity();
#if defined(TARGET_IPHONE)
		glFrustumf(-width, width, -width / aspect, width / aspect, n, f);
#elif defined(TARGET_WINDOWS)
		glFrustum(-width, width, -width / aspect, width / aspect, n, f);
#endif

		const GLint X = static_cast<GLint>(displayRect.position.x);
		const GLint Y = static_cast<GLint>(displayRect.position.y);
		const GLsizei W = static_cast<GLsizei>(screenSize.x * screenToDisplayScale);
		const GLsizei H = static_cast<GLsizei>(screenSize.y * screenToDisplayScale);
		viewport(X, Y, W, H);

		GRAPH2D_CHECK_GL_ERROR();
	}
#define TEST 0
	void GraphicDevice::ortho2D()
	{
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
#if TEST
		const GLint X = static_cast<GLint>(displayRect.position.x);
		const GLint Y = static_cast<GLint>(displayRect.position.y);
		const GLsizei W = static_cast<GLsizei>(screenSize.x * screenToDisplayScale);
		const GLsizei H = static_cast<GLsizei>(screenSize.y * screenToDisplayScale);

		glOrthof(X, X + W, Y + H, Y, 0, 1);

		viewport(0, 0, W, H);
#else
#if defined(TARGET_IPHONE)
		glOrthof(0, displayRect.size.x, displayRect.size.y, 0, 0, 1);
#else
		glOrtho(0, displayRect.size.x, displayRect.size.y, 0, 0, 1);
#endif
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();

		viewport(0, 0, static_cast<int>(displayRect.size.x), static_cast<int>(displayRect.size.y));
#endif
		GRAPH2D_CHECK_GL_ERROR();
	}

	void GraphicDevice::enableAlpha(const bool enable)
	{
		if(lastEnableAlpha != enable)
		{
			flush();

			if(enable)
				glEnable(GL_ALPHA_TEST);
			else
				glDisable(GL_ALPHA_TEST);
			lastEnableAlpha = enable;
			GRAPH2D_CHECK_GL_ERROR();
		}
	}

	void GraphicDevice::enableDepth(const bool enable)
	{
		if(lastEnableDepth != enable)
		{
			flush();

			if(enable)
				glEnable(GL_DEPTH_TEST);
			else
				glDisable(GL_DEPTH_TEST);
			lastEnableDepth = enable;
			GRAPH2D_CHECK_GL_ERROR();
		}
	}

	void GraphicDevice::enableOpaque(const bool enable)
	{
		if(lastEnableOpaque != enable)
		{
			flush();

			if(enable)
				glDisable(GL_BLEND);
			else
				glEnable(GL_BLEND);
			lastEnableOpaque = enable;
			GRAPH2D_CHECK_GL_ERROR();
		}
	}

	void GraphicDevice::enableScissor(const bool enable)
	{
		if(lastEnableScissor != enable)
		{
			flush();
#if 0
			if(enable)
				glEnable(GL_SCISSOR_TEST);
			else
				glDisable(GL_SCISSOR_TEST);
#endif
			lastEnableScissor = enable;
			GRAPH2D_CHECK_GL_ERROR();
		}
	}

	void GraphicDevice::enableTexture(const bool enable)
	{
		if(lastEnableTexture != enable)
		{
			flush();

			if(enable)
				glEnable(GL_TEXTURE_2D);
			else
				glDisable(GL_TEXTURE_2D);
			lastEnableTexture = enable;
			GRAPH2D_CHECK_GL_ERROR();
		}
	}

	void GraphicDevice::enableColorArray(const bool enable)
	{
		if(lastEnableColorArray != enable)
		{
			flush();

			if(enable)
				glEnableClientState(GL_COLOR_ARRAY);
			else
				glDisableClientState(GL_COLOR_ARRAY);
			lastEnableColorArray = enable;
			GRAPH2D_CHECK_GL_ERROR();
		}
	}

	void GraphicDevice::enableTextureCoordArray(const bool enable)
	{
		enableTexture(enable);

		if(lastEnableTextureCoordArray != enable)
		{
			flush();

			if(enable)
				glEnableClientState(GL_TEXTURE_COORD_ARRAY);
			else
				glDisableClientState(GL_TEXTURE_COORD_ARRAY);
			lastEnableTextureCoordArray = enable;
			GRAPH2D_CHECK_GL_ERROR();
		}
	}

	void GraphicDevice::getScissor(Vector2& position, Vector2& size)
	{
		position = lastEnableScissorPosition;
		size = lastEnableScissorSize;
	}

	void GraphicDevice::setScissor(const Vector2& position, const Vector2& size)
	{
		if(lastEnableScissorPosition != position || lastEnableScissorSize != size)
		{
			flush();
#if 0
			// glScissorは画面の下からの位置になるので注意が必要
			GRAPH2D_CALL_GL(glScissor(
				static_cast<GLint>(position.x * screenToDisplayScale),
				static_cast<GLint>((displayRect.size.y - (position.y + size.y) * screenToDisplayScale)),
				static_cast<GLsizei>(size.x * screenToDisplayScale),
				static_cast<GLsizei>(size.y * screenToDisplayScale)
			));
#endif
			lastEnableScissorPosition = position;
			lastEnableScissorSize = size;
		}
	}

	void GraphicDevice::setClearColor(const Color& color)
	{
#if defined(TARGET_WINDOWS) || defined(TARGET_APPLE) || defined(TARGET_LINUX)
		glClearColor(color.r, color.g, color.b, color.a);
#endif
	}

	const Color& GraphicDevice::getColor()
	{
		return lastPrimitiveColor;
	}

	void GraphicDevice::setColor(const Color& color)
	{
		if(lastPrimitiveColor != color)
		{
			flush();

			GRAPH2D_CALL_GL(glColor4f(color.r, color.g, color.b, color.a));
			lastPrimitiveColor = color;
		}
	}

	void GraphicDevice::setAlphaFunc(const GraphicDevice::AlphaFunc alphaFunc, const float value)
	{
		if(lastAlphaFunc != alphaFunc || lastAlphaFuncValue != value)
		{
			flush();

			static const GLenum funcs[] = {
				GL_NEVER,
				GL_LESS,
				GL_LEQUAL,
				GL_EQUAL,
				GL_NOTEQUAL,
				GL_GEQUAL,
				GL_GREATER,
				GL_ALWAYS
			};
			GRAPH2D_CALL_GL(glAlphaFunc(funcs[alphaFunc], value));

			lastAlphaFunc = static_cast<unsigned char>(alphaFunc);
			lastAlphaFuncValue = value;
		}
	}

	void GraphicDevice::setBlendMode(const GraphicDevice::BlendMode blendMode)
	{
		if(lastBlendMode != blendMode)
		{
			flush();

			// color = src * src_factor + dest * dest_factor
			switch(blendMode)
			{
			case BLEND_MODE_NORMAL:
				GRAPH2D_CALL_GL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));	// 通常
				break;
			case BLEND_MODE_ADD:
				GRAPH2D_CALL_GL(glBlendFunc(GL_SRC_ALPHA, GL_ONE));					// 加算
				break;
			case BLEND_MODE_MULTI:
				GRAPH2D_CALL_GL(glBlendFunc(GL_ZERO, GL_SRC_COLOR));				// 乗算(アルファは考慮できません)
				break;
			case BLEND_MODE_SCREEN:
				GRAPH2D_CALL_GL(glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ONE));		// スクリーン(アルファは考慮できません)
				break;
			}
			lastBlendMode = static_cast<unsigned char>(blendMode);
		}
	}

	void GraphicDevice::clear()
	{
#if defined(TARGET_WINDOWS) || defined(TARGET_APPLE) || defined(TARGET_LINUX)
		glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
#endif
	}

	// TODO:矩形描画を考慮してください
	void GraphicDevice::drawRect()
	{
#if defined(TARGET_IPHONE)
		int rect[4] = {0, 128, 128, -128};
		glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_CROP_RECT_OES, rect);
		glDrawTexfOES(50.0f, 100.0f, 0, 128.0f, 128.0f);
#endif
	}

	void GraphicDevice::begin(const unsigned char type, const GLenum mode, const Color& color)
	{
		if(lastPrimitiveType != type || lastPrimitiveMode != mode || lastPrimitiveColor != color)
			flush();

		setColor(color);

		enableOpaque((type & TYPE_OPAQUE) ? true : false);
		//enableScissor((type & TYPE_SCISSOR) ? true : false);

		enableColorArray((type & TYPE_COLOR) ? true : false);
		enableTextureCoordArray((type & TYPE_TEXTURE_COORDINATE) ? true : false);

		lastPrimitiveType = type;
		lastPrimitiveMode = mode;
	}

	void GraphicDevice::end()
	{
		if(
			lastPrimitiveType == TRIANGLE_STRIP ||
			colorCount >= VERTEX_MAX_COUNT ||
			coordinateCount >= VERTEX_MAX_COUNT ||
			vertexCount >= VERTEX_MAX_COUNT
		)
			flush();
	}

	void GraphicDevice::addColor(const Color& color)
	{
		assert(colorCount < VERTEX_MAX_COUNT);

		const GLsizei index = colorCount * 4;
		colors[index + 0] = static_cast<GLubyte>(color.r * 255.0f);
		colors[index + 1] = static_cast<GLubyte>(color.g * 255.0f);
		colors[index + 2] = static_cast<GLubyte>(color.b * 255.0f);
		colors[index + 3] = static_cast<GLubyte>(color.a * 255.0f);
		++colorCount;
	}

	void GraphicDevice::addTextureCoord(const Vector2& textureCoord)
	{
		assert(coordinateCount < VERTEX_MAX_COUNT);

		const GLsizei index = coordinateCount * 2;
		coordinates[index + 0] = textureCoord.x;
		coordinates[index + 1] = textureCoord.y;
		++coordinateCount;
	}

	void GraphicDevice::addVertex(const Vector2& vertex)
	{
		assert(vertexCount < VERTEX_MAX_COUNT);
		const GLsizei index = vertexCount * 2;
#if TEST
		const float X = vertex.x * screenToDisplayScale;
		const float Y = vertex.y * screenToDisplayScale;
#else
		const float X = displayRect.position.x + vertex.x * screenToDisplayScale;
		const float Y = displayRect.position.y + vertex.y * screenToDisplayScale;
#endif
		verteces[index + 0] = X;
		verteces[index + 1] = Y;

		++vertexCount;
	}

	void GraphicDevice::flush()
	{
#if defined(DEBUG) || defined(_DEBUG) || defined(_DEBUG_) || !defined(NDEBUG)
		if(vertexCount > VERTEX_MAX_COUNT || colorCount > VERTEX_MAX_COUNT || coordinateCount > VERTEX_MAX_COUNT)
		{
			printf("GraphicDevice: vertex buffer over flow.\n");
			printf("color: %d\n", colorCount);
			printf("texture coordinate: %d\n", coordinateCount);
			printf("vertex: %d\n", vertexCount);
		}
#if 0
		{
			GLboolean enable;
			glGetBooleanv(GL_VERTEX_ARRAY, &enable);
			assert(enable == GL_TRUE);
			/*
			 glGetBooleanv(GL_NORMAL_ARRAY, &enable);
			assert(enable == GL_FALSE);
			 */
			glGetBooleanv(GL_COLOR_ARRAY, &enable);
			assert(enable ? true : false == lastEnableColorArray);
			glGetBooleanv(GL_TEXTURE_COORD_ARRAY, &enable);
			assert(enable ? true : false == lastEnableTextureCoordArray);
   		}
#endif
#endif

		if(vertexCount > 0)
		{
			GRAPH2D_CALL_GL(glDrawArrays(lastPrimitiveMode, 0, vertexCount));
			vertexCount = 0;
		}
		colorCount = coordinateCount = 0;
	}

	void GraphicDevice::finish()
	{
		flush();
		GRAPH2D_CALL_GL(glFinish());
	}
}
